前幾天已經學過 Kotlin 類別、物件、建構子與繼承。今天繼續進入 Kotlin 物件導向的進階內容,包含抽象類別、介面、多介面實作、巢狀類別、內部類別、單例物件、伴生物件、匿名物件與函式式介面。參考資料:Kotlin-Classes
今天會學到:
abstract 建立抽象類別。interface 定義類別必須具備的能力。object 建立單例物件。companion object 建立類別層級的成員。fun interface 與 Lambda 簡化程式。參考資料:Kotlin-Interfaces|Kotlin-Object declarations
抽象類別是一種不能直接建立物件的類別,主要用途是定義一組共同的屬性與函式,再交由子類別完成實際內容。宣告抽象類別時,需要在 class 前面加上 abstract。參考資料:Kotlin-Abstract classes
abstract class Base
抽象類別中可以同時包含:
抽象屬性和抽象函式只規定「必須有什麼」,不提供完整實作;繼承它的子類別必須使用 override 完成這些成員。參考資料:Kotlin-Inheritance
以下是上傳資料 P11-28_As(1).kt 的抽象類別範例,經過命名與輸出方式整理後如下。參考資料:Kotlin-Abstract classes
/**
* 抽象父類別 Base。
*
* 抽象類別不能直接建立物件,
* 主要用來規範子類別必須實作的成員。
*/
abstract class Base {
// 抽象屬性沒有初始值,必須由子類別實作。
abstract var a: Int
// 一般屬性已經提供初始值,子類別可以直接使用。
var b: Int = 0
// 抽象函式沒有函式內容,子類別必須覆寫。
abstract fun fun1()
// 一般函式已經完成實作,子類別可以直接呼叫。
fun fun2() {
println("From Base class")
}
}
/**
* Derived 繼承抽象類別 Base。
*
* 因為 Base 包含抽象屬性 a 與抽象函式 fun1(),
* 所以 Derived 必須使用 override 完成實作。
*/
class Derived : Base() {
// 完成父類別規定的抽象屬性。
override var a: Int = 0
// 完成父類別規定的抽象函式。
override fun fun1() {
println("From Derived class")
}
}
fun main() {
// Base 是抽象類別,不能寫成 Base()。
// 必須建立具體子類別 Derived 的物件。
val myClass = Derived()
println("a = ${myClass.a}")
println("b = ${myClass.b}")
// fun1() 與 fun2() 的回傳型別都是 Unit,
// 因此直接呼叫即可,不需要放進 println()。
myClass.fun1()
myClass.fun2()
}
執行結果:
a = 0
b = 0
From Derived class
From Base class
原始參考程式使用以下寫法:
println(myClass.fun1())
println(myClass.fun2())
因為 fun1() 和 fun2() 沒有宣告其他回傳型別,所以回傳型別為 Unit。這樣寫除了函式原本的輸出之外,還會再印出 kotlin.Unit。如果只是想執行函式,直接呼叫即可。參考資料:Kotlin-Functions
第一份練習要求建立 Shape 抽象類別,再建立 Circle 與 Rectangle 子類別,分別覆寫 area() 計算圓形與矩形面積。參考資料:Kotlin-Inheritance
import kotlin.math.PI
/**
* 所有形狀共同的抽象父類別。
*
* 每一種具體形狀都必須提供自己的面積計算公式。
*/
abstract class Shape {
/**
* 計算形狀面積。
*
* @return 形狀面積
*/
abstract fun area(): Double
}
/**
* 圓形。
*
* @property radius 圓形半徑
*/
class Circle(
private val radius: Double
) : Shape() {
/**
* 圓面積公式:π × 半徑平方。
*/
override fun area(): Double {
return PI * radius * radius
}
}
/**
* 矩形。
*
* @property width 矩形寬度
* @property height 矩形高度
*/
class Rectangle(
private val width: Double,
private val height: Double
) : Shape() {
/**
* 矩形面積公式:寬 × 高。
*/
override fun area(): Double {
return width * height
}
}
fun main() {
// 建立半徑為 6 的圓形。
val circle = Circle(radius = 6.0)
// 建立寬 12、高 16 的矩形。
val rectangle = Rectangle(
width = 12.0,
height = 16.0
)
println("圓形面積 = %.2f".format(circle.area()))
println("矩形面積 = %.2f".format(rectangle.area()))
}
執行結果:
圓形面積 = 113.10
矩形面積 = 192.00
Shape 不知道每種圖形的面積公式,因此只定義 area()。真正的公式分別放在 Circle 與 Rectangle,這就是抽象類別的核心精神:父類別規範共同結構,子類別完成不同實作。參考資料:Kotlin-Abstract classes
第二個抽象類別練習是建立員工薪資系統。正職員工與兼職員工都是員工,因此可以共同繼承 Employee;但兩種員工的薪資算法不同,所以交由各個子類別覆寫 calculateSalary()。參考資料:Kotlin-Inheritance
/**
* 員工抽象父類別。
*
* @property name 員工姓名
*/
abstract class Employee(
val name: String
) {
/**
* 計算員工薪資。
*
* 不同員工類型具有不同計算方式,
* 因此交由子類別完成。
*/
abstract fun calculateSalary(): Double
}
/**
* 正職員工。
*
* @property name 員工姓名
* @property monthlySalary 每月薪資
*/
class FullTimeEmployee(
name: String,
private val monthlySalary: Double
) : Employee(name) {
/**
* 計算正職員工年薪。
*
* 年薪=月薪 × 12。
*/
override fun calculateSalary(): Double {
return monthlySalary * 12
}
}
/**
* 兼職員工。
*
* @property name 員工姓名
* @property hourlyRate 每小時薪資
* @property workingHours 當月工作時數
*/
class PartTimeEmployee(
name: String,
private val hourlyRate: Double,
private val workingHours: Int
) : Employee(name) {
/**
* 計算兼職員工月薪。
*
* 月薪=時薪 × 工作時數。
*/
override fun calculateSalary(): Double {
return hourlyRate * workingHours
}
}
fun main() {
// Alice 是正職員工,月薪為 50,000 元。
val fullTimeEmployee = FullTimeEmployee(
name = "Alice",
monthlySalary = 50_000.0
)
// Bob 是兼職員工,時薪 300 元、工作 60 小時。
val partTimeEmployee = PartTimeEmployee(
name = "Bob",
hourlyRate = 300.0,
workingHours = 60
)
println(
"${fullTimeEmployee.name} 的年薪為:${
"%.2f".format(fullTimeEmployee.calculateSalary())
} 元"
)
println(
"${partTimeEmployee.name} 的月薪為:${
"%.2f".format(partTimeEmployee.calculateSalary())
} 元"
)
}
執行結果:
Alice 的年薪為:600000.00 元
Bob 的月薪為:18000.00 元
因為兩個物件都繼承 Employee,所以還能放進同一個 List<Employee>,再透過相同的 calculateSalary() 呼叫不同實作,這就是多型。參考資料:Kotlin-Inheritance
val employees: List<Employee> = listOf(
FullTimeEmployee("Alice", 50_000.0),
PartTimeEmployee("Bob", 300.0, 60)
)
employees.forEach { employee ->
println(
"${employee.name}:${
"%.2f".format(employee.calculateSalary())
} 元"
)
}
介面使用 interface 宣告,用來定義類別應具備的能力或行為。例如「可以繪圖」、「可以點擊」、「可以計算面積」或「可以付款」。參考資料:Kotlin-Interfaces
interface Drawable {
fun draw()
}
實作介面的類別需要在類別名稱後面加入冒號及介面名稱:
class SimpleCircle : Drawable
如果介面的函式沒有提供預設內容,實作類別就必須使用 override 完成它:
class SimpleCircle : Drawable {
override fun draw() {
println("Drawing a circle")
}
}
介面與一般父類別不同,一個類別只能直接繼承一個父類別,卻可以同時實作多個介面。參考資料:Kotlin-Interfaces
上傳的 interface(1).kt 定義了 IntB 介面。它要求實作類別提供數學和英文成績,並提供完成的 show() 預設函式。參考資料:Kotlin-Properties in interfaces
/**
* 成績顯示介面。
*
* 介面可以宣告抽象屬性,
* 但不能像一般類別一樣直接保存實例狀態。
*/
interface IntB {
// 實作類別必須提供數學成績。
var math: Int
// 實作類別必須提供英文成績。
var eng: Int
/**
* 介面也可以提供具有函式內容的預設實作。
*/
fun show() {
println("math = $math, eng = $eng")
}
}
/**
* Report 透過主建構子直接實作介面的兩個屬性。
*/
class Report(
override var math: Int = 0,
override var eng: Int = 0
) : IntB
fun main() {
val report = Report(
math = 95,
eng = 88
)
// Report 沒有重新覆寫 show(),
// 因此會直接使用 IntB 提供的預設實作。
report.show()
}
執行結果:
math = 95, eng = 88
Report 沒有函式主體也沒有覆寫 show(),仍然可以呼叫 show(),因為 IntB 已經提供預設實作。參考資料:Kotlin-Implementing interfaces
上傳的 interface2(1).kt 使用 Drawable 與 Click 介面,讓 Button 同時具備繪圖和點擊能力。原始資料中的 Darwable 應是 Drawable 的拼字錯誤,以下程式已經修正。參考資料:Kotlin-Interfaces
/**
* 定義可以繪製的能力。
*/
interface Drawable {
fun draw()
}
/**
* 定義可以點擊的能力。
*/
interface Click {
fun click()
}
/**
* SimpleCircle 只具備繪圖能力。
*/
class SimpleCircle : Drawable {
override fun draw() {
println("Drawing a circle")
}
}
/**
* Button 同時實作 Drawable 與 Click。
*
* Kotlin 使用逗號分隔多個介面。
*/
open class Button : Drawable, Click {
override fun draw() {
println("Drawing a button")
}
override fun click() {
println("Button clicked")
}
}
fun main() {
val circle = SimpleCircle()
circle.draw()
val button = Button()
button.draw()
button.click()
}
執行結果:
Drawing a circle
Drawing a button
Button clicked
這個範例可以把介面理解成不同能力的組合:
| 類別 | Drawable |
Click |
|---|---|---|
SimpleCircle |
有 | 無 |
Button |
有 | 有 |
Button 不需要為了取得兩種能力而同時繼承兩個類別,只要實作兩個介面即可。參考資料:Kotlin-Interfaces
抽象類別能定義共同狀態和行為;如果只希望規定類別必須提供哪些能力,也可以使用介面。上傳資料中的 Shape 介面要求所有形狀都提供 area() 與 perimeter()。參考資料:Kotlin-Interfaces
import kotlin.math.PI
/**
* 所有形狀必須具備面積與周長計算能力。
*/
interface Shape {
fun area(): Double
fun perimeter(): Double
}
/**
* 圓形實作 Shape 介面。
*
* @property radius 圓形半徑
*/
class Circle(
private val radius: Double
) : Shape {
/**
* 圓周長=2 × π × 半徑。
*/
override fun perimeter(): Double {
return 2 * PI * radius
}
/**
* 圓面積=π × 半徑平方。
*/
override fun area(): Double {
return PI * radius * radius
}
/**
* 顯示完整的圓形資料。
*/
fun info() {
println("圓形半徑 = $radius")
println("圓形面積 = %.2f".format(area()))
println("圓形周長 = %.2f".format(perimeter()))
}
}
fun main() {
val circle = Circle(radius = 3.0)
circle.info()
}
執行結果:
圓形半徑 = 3.0
圓形面積 = 28.27
圓形周長 = 18.85
參考圖片中的 Report 同時實作 IntA 和 IntB。如果兩個介面都提供名稱與參數完全相同的 show() 預設函式,Kotlin 無法自行決定要採用哪一個版本,因此 Report 必須覆寫 show()。參考資料:Kotlin-Resolving overriding conflicts

完整範例如下:
/**
* 學生基本資料介面。
*/
interface IntA {
var name: String
var id: Int
/**
* 顯示學生姓名與學號。
*/
fun show() {
println("name = $name, id = $id")
}
}
/**
* 學生成績介面。
*/
interface IntB {
var math: Int
var eng: Int
/**
* 顯示數學與英文成績。
*/
fun show() {
println("math = $math, eng = $eng")
}
}
/**
* Report 同時實作 IntA 與 IntB。
*
* 因為兩個介面都有 show(),
* 所以 Report 必須自行覆寫並解決衝突。
*/
class Report(
override var name: String,
override var id: Int,
override var math: Int,
override var eng: Int
) : IntA, IntB {
override fun show() {
// 指定呼叫 IntA 版本的 show()。
super<IntA>.show()
println("score:")
// 指定呼叫 IntB 版本的 show()。
super<IntB>.show()
}
}
fun main() {
val report = Report(
name = "Alex",
id = 1001,
math = 95,
eng = 88
)
report.show()
}
執行結果:
name = Alex, id = 1001
score:
math = 95, eng = 88
以下語法代表明確指定要呼叫哪個介面的預設函式:
super<IntA>.show()
super<IntB>.show()
如果只寫成 super.show(),編譯器不知道要選擇 IntA.show() 還是 IntB.show(),因此會產生錯誤。參考資料:Kotlin-Resolving overriding conflicts
抽象類別和介面都能規定子類別必須實作哪些成員,但用途並不完全相同。參考資料:Kotlin-Classes|Kotlin-Interfaces
| 比較項目 | 抽象類別 | 介面 |
|---|---|---|
| 宣告方式 | abstract class |
interface |
| 能否直接建立物件 | 不可以 | 不可以 |
| 建構子 | 可以有 | 不可以有一般建構子 |
| 保存物件狀態 | 可以 | 介面屬性不能直接保存 backing field |
| 一般函式 | 可以 | 可以提供預設實作 |
| 抽象函式 | 可以 | 可以 |
| 類別可使用的數量 | 只能繼承一個父類別 | 可以實作多個介面 |
| 適合用途 | 建立具有共同狀態的類別家族 | 定義類別的能力或契約 |
簡單來說:
例如 Circle 是一種 Shape,可以設計成繼承關係;Button 可以繪圖也可以點擊,則可以實作 Drawable 與 Click。實際設計仍需依資料狀態、擴充需求與類別關係判斷。參考資料:Kotlin-Inheritance|Kotlin-Interfaces
Kotlin 在一個類別中宣告另一個類別時,預設建立的是巢狀類別。巢狀類別不會持有外部類別物件的參考,因此不能直接存取外部類別的屬性或函式。參考資料:Kotlin-Nested classes
/**
* 外部類別。
*/
class Outer {
val message = "Hello from Outer"
/**
* 巢狀類別。
*
* 它不會持有 Outer 物件的參考,
* 因此不能直接讀取 message。
*/
class Nested {
fun greet(): String {
return "Hello from Nested"
}
}
}
fun main() {
val outer = Outer()
println(outer.message)
// Nested 不需要先建立 Outer 物件,
// 直接透過 Outer.Nested() 建立即可。
val nested = Outer.Nested()
println(nested.greet())
}
執行結果:
Hello from Outer
Hello from Nested
巢狀類別的建立方式是:
val nested = Outer.Nested()
不能透過 outer.Nested() 建立,因為 Nested 不屬於某個特定的 Outer 物件。參考資料:Kotlin-Nested classes
如果希望巢狀的類別能夠存取外部物件,就要在類別前加入 inner。內部類別會持有外部類別實例的參考,因此能直接使用外部類別的屬性和函式。參考資料:Kotlin-Inner classes
/**
* 包含內部類別的外部類別。
*/
class OuterIn {
val message = "Hello from OuterIn"
fun outerFun() {
println("This is OuterIn function")
}
/**
* 使用 inner 宣告內部類別。
*
* Inner 能直接存取 OuterIn 的成員。
*/
inner class Inner {
/**
* 直接讀取外部類別的 message。
*/
fun greet(): String {
return "Message is $message"
}
/**
* 直接呼叫外部類別的 outerFun()。
*/
fun innerFun() {
outerFun()
}
}
}
fun main() {
// 必須先建立外部類別物件。
val outerIn = OuterIn()
outerIn.outerFun()
// 內部類別屬於特定的 outerIn 物件,
// 因此透過該物件建立 Inner。
val inner = outerIn.Inner()
println(inner.greet())
inner.innerFun()
}
執行結果:
This is OuterIn function
Message is Hello from OuterIn
This is OuterIn function
巢狀類別與內部類別的差異如下。參考資料:Kotlin-Nested and inner classes
| 比較項目 | Nested Class | Inner Class |
|---|---|---|
| 宣告方式 | class Nested |
inner class Inner |
| 持有外部物件參考 | 不會 | 會 |
| 存取外部屬性 | 不可以直接存取 | 可以直接存取 |
| 建立方式 | Outer.Nested() |
outer.Inner() |
| 記憶體關係 | 與外部物件分開 | 依附於外部物件 |
Android 的共用點擊事件監聽器經常使用 inner class,因為監聽器需要讀取 Activity 的 binding 或其他畫面元件。不過,內部類別會持有 Activity 參考,所以不應讓監聽器的生命週期超過 Activity。參考資料:Android Developers-Memory overview
使用一般類別時,每次呼叫建構子都會建立一個新物件;使用 object 宣告時,整個程式只會共用同一個物件,因此很適合建立記錄器、設定管理器或共用工具。參考資料:Kotlin-Object declarations
/**
* 全程共用的記錄器。
*
* Logger 是單例物件,不需要也不能使用 Logger() 建立。
*/
object Logger {
// 記錄 log() 被呼叫的次數。
private var count: Int = 0
/**
* object 第一次被存取時執行初始化區塊。
*/
init {
println("Logger initialized")
}
/**
* 輸出記錄並增加次數。
*/
fun log(message: String) {
println("Log: $message")
count++
}
/**
* 取得目前記錄次數。
*/
fun logCount(): Int {
return count
}
}
fun main() {
println("count = ${Logger.logCount()}")
Logger.log("Log test 1")
println("count = ${Logger.logCount()}")
Logger.log("Log test 2")
println("count = ${Logger.logCount()}")
}
執行結果:
Logger initialized
count = 0
Log: Log test 1
count = 1
Log: Log test 2
count = 2
呼叫單例物件時,直接使用物件名稱:
Logger.log("開始執行")
不能寫成:
val logger = Logger()
因為 object 本身就是唯一的實例,不會提供一般建構子。參考資料:Kotlin-Object declarations
上傳資料也使用 object 建立亂數產生器。以下版本保留原本功能,並改善物件名稱與參數命名。參考資料:Kotlin-Object declarations
/**
* 共用的亂數產生器。
*/
object RandomNumberGenerator {
/**
* 要產生的亂數數量。
*
* setter 會確保數量至少為 1。
*/
var numberCount: Int = 1
set(value) {
field = if (value < 1) {
1
} else {
value
}
}
/**
* 產生指定數量、範圍為 1 到 100 的亂數。
*/
fun generateNumbers(): IntArray {
return IntArray(numberCount).apply {
for (index in indices) {
this[index] = (1..100).random()
}
}
}
}
fun main() {
RandomNumberGenerator.numberCount = 5
val numbers =
RandomNumberGenerator.generateNumbers()
numbers.forEachIndexed { index, number ->
println("numbers[$index] = $number")
}
}
如果把 numberCount 設為 0 或負數,自訂 setter 會自動將它改成 1。setter 中的 field 代表該屬性真正保存數值的 backing field;如果在 setter 裡再次寫 numberCount = value,就會不斷呼叫自己而造成無限遞迴。參考資料:Kotlin-Getters and setters
companion object 宣告在類別內部,裡面的成員可以透過類別名稱直接存取,不必先建立類別物件。它常被用來放常數、工廠方法,以及 Android Fragment 的 newInstance()。參考資料:Kotlin-Companion objects
/**
* 使用伴生物件建立 User。
*/
class User private constructor(
val name: String
) {
companion object {
const val TAG = "User"
/**
* 工廠方法:統一負責建立 User。
*/
fun create(name: String): User {
return User(name)
}
}
fun showInfo() {
println("TAG = $TAG, name = $name")
}
}
fun main() {
// 不需要建立 companion object,
// 直接使用類別名稱呼叫 create()。
val user = User.create("Alice")
user.showInfo()
// 常數也能透過類別名稱存取。
println(User.TAG)
}
執行結果:
TAG = User, name = Alice
User
伴生物件看起來很像 Java 的 static,但實際上它仍然是一個物件,也能實作介面。參考資料:Kotlin-Companion objects
上傳的 Object_2(1).kt 示範使用伴生物件建立 Fragment。實際 Android 開發時,Fragment 的輸入參數應放入 arguments 的 Bundle,這樣系統重新建立 Fragment 時才能還原資料。參考資料:Android Developers-Pass data between fragments
import android.os.Bundle
import androidx.fragment.app.Fragment
/**
* 顯示詳細資料的 Fragment。
*/
class DetailFragment : Fragment() {
companion object {
private const val ARG_PARAM_1 = "param1"
private const val ARG_PARAM_2 = "param2"
/**
* 建立 DetailFragment 並保存傳入參數。
*/
fun newInstance(
param1: String,
param2: String
): DetailFragment {
return DetailFragment().apply {
arguments = Bundle().apply {
putString(ARG_PARAM_1, param1)
putString(ARG_PARAM_2, param2)
}
}
}
}
/**
* 從 arguments 讀取參數。
*/
fun readParameters() {
val param1 =
arguments?.getString(ARG_PARAM_1)
val param2 =
arguments?.getString(ARG_PARAM_2)
println("param1 = $param1")
println("param2 = $param2")
}
}
建立 Fragment 時可以直接呼叫:
val detailFragment = DetailFragment.newInstance(
param1 = "Value 1",
param2 = "Value 2"
)
不建議只將 Fragment 參數存成一般屬性,因為 Fragment 被系統重新建立後,一般屬性可能回到初始狀態;使用 arguments 才能配合 Fragment 的生命週期保存建立參數。參考資料:Android Developers-Fragment lifecycle
如果只需要使用一次某個類別的特殊版本,就不一定要另外宣告新類別,可以使用 object : 類別或介面 建立匿名物件。參考資料:Kotlin-Object expressions
/**
* 可被繼承的汽車類別。
*/
open class Car(
val brand: String
) {
open fun basicInfo(name: String): String {
println("Car brand = $brand, name = $name")
return "Salesperson name is $name"
}
}
fun main() {
// 建立一般 Car 物件。
var myCar: Car = Car("BMW")
println(myCar.basicInfo("John"))
println()
// 使用匿名物件建立一個特殊版本的 Car,
// 不必再額外宣告 ToyotaCar 類別。
myCar = object : Car("Toyota") {
override fun basicInfo(name: String): String {
println("This car brand is $brand")
return "Salesperson name is $name"
}
}
println(myCar.basicInfo("Mary"))
}
匿名物件也可以直接實作介面:
interface Click {
fun click()
}
fun main() {
// 建立只使用一次的 Click 實作。
val button = object : Click {
override fun click() {
println("Button clicked")
}
}
button.click()
}
匿名物件適合一次性、內容短小的實作。如果相同邏輯會重複使用,建立有名稱的類別通常更容易閱讀與測試。參考資料:Kotlin-Object expressions
如果介面只包含一個抽象函式,可以使用 fun interface 宣告函式式介面。Kotlin 能透過 SAM 轉換,直接使用 Lambda 建立該介面的實作。參考資料:Kotlin-Functional interfaces
/**
* 只包含一個抽象函式的函式式介面。
*/
fun interface Action {
fun execute()
}
/**
* 接收任何 Action 並執行。
*/
fun runAction(action: Action) {
println("Start runAction")
action.execute()
println("End runAction")
}
fun main() {
// 因為 Action 是 fun interface,
// 所以可以直接使用 Lambda 實作 execute()。
runAction {
println("Executing action 1")
}
println()
runAction {
println("Executing action 2")
}
}
執行結果:
Start runAction
Executing action 1
End runAction
Start runAction
Executing action 2
End runAction
它也可以使用完整匿名物件寫法:
runAction(
object : Action {
override fun execute() {
println("Executing anonymous implementation")
}
}
)
但在只有一個抽象函式而且邏輯很短的情況下,Lambda 通常更簡潔。參考資料:Kotlin-SAM conversions
上傳資料中有以下寫法:
runAction {
object : Action {
override fun execute() {
println("Executing implementation!")
}
}
}
最外層 Lambda 並沒有執行匿名物件的 execute(),只是在 Lambda 中建立一個物件後就結束,因此看不到 Executing implementation!。正確方式可以直接使用 Lambda:
runAction {
println("Executing implementation!")
}
或者建立匿名物件後直接傳入:
runAction(
object : Action {
override fun execute() {
println("Executing implementation!")
}
}
)
參考資料:Kotlin-Functional interfaces
原始 DoAction 還有一個 TODO():
private fun action() {
TODO("Not yet implemented")
}
TODO() 會直接丟出 NotImplementedError,所以程式執行到這裡就會中止。應該先完成函式內容:
class DoAction : Action {
override fun execute() {
println("Executing DoAction!")
action()
}
/**
* 已完成實作,不再使用 TODO()。
*/
private fun action() {
println("DoAction internal action")
}
}
參考資料:Kotlin-TODO
ex_9.txt 要求建立信用卡與現金付款。這個題目很適合使用策略模式:Checkout 不需要知道各種付款方式的內部細節,只要接收一個符合 PaymentMethod 介面的物件。參考資料:Kotlin-Interfaces
/**
* 所有付款方式共同遵守的介面。
*/
interface PaymentMethod {
/**
* 執行付款。
*
* @param amount 付款金額
* @return true 表示付款成功,false 表示付款失敗
*/
fun pay(amount: Int): Boolean
}
/**
* 信用卡付款策略。
*/
class CreditCard : PaymentMethod {
override fun pay(amount: Int): Boolean {
// 金額必須大於零。
if (amount <= 0) {
println("信用卡付款失敗:金額必須大於 0 元")
return false
}
println("使用信用卡付款:$amount 元")
return true
}
}
/**
* 現金付款策略。
*/
class Cash : PaymentMethod {
override fun pay(amount: Int): Boolean {
// 金額必須大於零。
if (amount <= 0) {
println("現金付款失敗:金額必須大於 0 元")
return false
}
println("使用現金付款:$amount 元")
return true
}
}
/**
* 結帳類別。
*
* Checkout 只依賴 PaymentMethod 介面,
* 不依賴特定的信用卡或現金類別。
*/
class Checkout {
/**
* 執行完整付款流程。
*
* @param amount 付款金額
* @param method 本次使用的付款策略
*/
fun processPayment(
amount: Int,
method: PaymentMethod
) {
val isSuccessful = method.pay(amount)
if (isSuccessful) {
println("付款成功")
} else {
println("付款失敗")
}
}
}
fun main() {
// 建立結帳系統。
val checkout = Checkout()
// 建立兩種付款策略。
val creditCard = CreditCard()
val cash = Cash()
// 使用信用卡支付 10,000 元。
checkout.processPayment(
amount = 10_000,
method = creditCard
)
println("--------------------")
// 使用現金支付 500 元。
checkout.processPayment(
amount = 500,
method = cash
)
}
執行結果:
使用信用卡付款:10000 元
付款成功
--------------------
使用現金付款:500 元
付款成功
Checkout 接收的型別是 PaymentMethod:
fun processPayment(
amount: Int,
method: PaymentMethod
)
因此未來新增行動支付時,只要再建立一個實作 PaymentMethod 的類別,不需要修改 Checkout 原本的付款流程。參考資料:Kotlin-Interfaces
/**
* 行動支付策略。
*/
class MobilePayment : PaymentMethod {
override fun pay(amount: Int): Boolean {
if (amount <= 0) {
println("行動支付失敗:金額必須大於 0 元")
return false
}
println("使用行動支付:$amount 元")
return true
}
}
這種設計降低了結帳流程與特定付款工具之間的耦合,也更容易加入新的付款方式。參考資料:Android Developers-Architecture recommendations
今天接觸了多種看似相近、用途卻不同的語法。參考資料:Kotlin-Object declarations and expressions
| 寫法 | 是否有類別名稱 | 物件數量 | 主要用途 |
|---|---|---|---|
class User |
有 | 可以建立多個 | 一般類別 |
abstract class Shape |
有 | 本身不能建立 | 規範子類別 |
interface Click |
有 | 本身不能建立 | 定義能力 |
class Nested |
有 | 可以建立多個 | 不依附外部物件 |
inner class Inner |
有 | 可以建立多個 | 需要存取外部物件 |
object Logger |
有 | 全程一個 | 單例物件 |
companion object |
隸屬於類別 | 每個類別一個 | 常數與工廠方法 |
object : Click |
沒有 | 當次建立一個 | 匿名物件 |
fun interface Action |
有 | 可用 Lambda 建立 | 單一抽象行為 |
今天從抽象類別開始,進一步學習了 Kotlin 組織物件與行為的不同方式。參考資料:Kotlin-Classes
abstract class 不能直接建立物件。interface 適合定義類別應具備的能力。super<IntA>.show() 可以指定呼叫特定介面的預設函式。inner class 可以直接存取外部類別成員。object 可建立全程共用的單例物件。companion object 適合放置常數及工廠方法。fun interface 可以透過 Lambda 簡化單一抽象函式。TODO() 尚未實作前,執行時會造成程式中止。參考資料:Kotlin-Interfaces|Kotlin-Nested classes|Kotlin-Object declarations
抽象類別、介面與物件宣告,是 Kotlin 物件導向設計中非常重要的一環。抽象類別可以整理具有共同狀態的類別家族;介面可以將繪圖、點擊、計算與付款等能力拆開;object 與 companion object 則能管理共用物件、常數及建立流程。參考資料:Kotlin 官方文件
在 Android 開發中,這些觀念隨處可見。例如 Activity 繼承 AppCompatActivity、點擊監聽器實作 View.OnClickListener、Fragment 使用 companion object 提供 newInstance(),以及透過介面替換資料來源、藍牙連線方式或付款策略。理解今天的內容後,未來閱讀 Android 架構程式會容易許多。參考資料:Android Developers-App architecture
下一篇可以正式回到 Android 畫面開發,認識 EditText、CheckBox、RadioButton、RadioGroup、Switch 與 Toast,進一步完成使用者資料輸入與選項互動。參考資料:Android Developers-Input controls